#include "nsock.h"
#include "nsock_internal.h"

#include <limits.h>
#if HAVE_SYS_IOCTL_H
#include <sys/ioctl.h>
#endif
#if HAVE_NET_BPF_H
#ifdef _AIX
/* Prevent bpf.h from redefining the DLT_ values to their IFT_ values. (See
   similar comment in libpcap/pcap-bpf.c.) */
#undef _AIX
#include <net/bpf.h>
#define _AIX
#else
#include <net/bpf.h>
#endif
#endif

#include "nsock_pcap.h"

extern struct timeval nsock_tod;

#if HAVE_PCAP
static int nsock_pcap_get_l3_offset(pcap_t *pt, int *dl);
char * nsock_pcap_set_filter(pcap_t *pt, const char *device, const char *bpf);

/*
 * Convert new nsiod to pcap descriptor. Other parameters have the
 * same meaning as for pcap_open_live in pcap(3).
 * 	device   : pcap-style device name
 * 	snaplen  : size of packet to be copied to hanler
 * 	promisc  : whether to open device in promiscous mode
 * 	bpf_fmt	 : berkeley filter 
 * return value: NULL if everything was okay, or error string if error occurred
 * */
char* nsock_pcap_open(nsock_pool nsp, nsock_iod nsiod, 
		const char *pcap_device, int snaplen, int promisc,
		const char *bpf_fmt, ...)
				
{
	msiod *nsi = (msiod *) nsiod;
	mspool *ms = (mspool *) nsp;
	mspcap *mp = (mspcap *) nsi->pcap;
	static char errorbuf[128];
	char err0r[PCAP_ERRBUF_SIZE];
	/* packet filter string */
	char bpf[4096];
	va_list ap;
	int failed, datalink;
	char *e;

	gettimeofday(&nsock_tod, NULL);

	#ifdef PCAP_CAN_DO_SELECT
	  #if PCAP_BSD_SELECT_HACK
	    /* MacOsX reports error if to_ms is too big (like INT_MAX) with error
	     *    FAILED. Reported error: BIOCSRTIMEOUT: Invalid argument  
	     * INT_MAX/6 (=357913941) seems to be working...*/
	    int to_ms = 357913941;
	  #else
	    int to_ms = 200;
	  #endif
	#else
	int to_ms = 1;
	#endif
	if(mp) return "nsock-pcap: this nsi already has pcap device opened";
	mp = (mspcap *)safe_malloc(sizeof(mspcap));
	nsi->pcap = (void*)mp;
	
	va_start(ap, bpf_fmt);
	if(Vsnprintf(bpf, sizeof(bpf), bpf_fmt, ap) >= (int) sizeof(bpf)){
		va_end(ap);
		return "nsock-pcap: nsock_pcap_open called with too-large bpf filter arg";
	}
	va_end(ap);
	
	if (ms->tracelevel > 0)
  		nsock_trace(ms, "PCAP requested on device '%s' with berkeley filter '%s' (promisc=%i snaplen=%i to_ms=%i) (IOD #%li)", 
  		pcap_device,bpf, promisc, snaplen, to_ms, nsi->id);

	failed = 0;
	do {
		mp->pt = pcap_open_live((char*)pcap_device, snaplen, promisc, to_ms, err0r);
		if (mp->pt)	/* okay, opened!*/ 
			break;
		/* sorry, something failed*/
		if (++failed >= 3) {
			mp->pcap_device = strdup(pcap_device);
			fprintf(stderr,
	"Call to pcap_open_live(%s, %d, %d, %d) failed three times. Reported error: %s\n"
	"There are several possible reasons for this, depending on your operating system:\n"
	"LINUX: If you are getting Socket type not supported, try modprobe af_packet or recompile your kernel with PACKET enabled.\n"
	"*BSD:  If you are getting device not configured, you need to recompile your kernel with Berkeley Packet Filter support.  If you are getting No such file or directory, try creating the device (eg cd /dev; MAKEDEV <device>; or use mknod).\n"
	"*WINDOWS:  Nmap only supports ethernet interfaces on Windows for most operations because Microsoft disabled raw sockets as of Windows XP SP2.  Depending on the reason for this error, it is possible that the --unprivileged command-line argument will help.\n"
	"SOLARIS:  If you are trying to scan localhost and getting '/dev/lo0: No such file or directory', complain to Sun.  I don't think Solaris can support advanced localhost scans.  You can probably use \"-PN -sT localhost\" though.\n\n", 
				pcap_device, snaplen, promisc, to_ms, err0r);
			return "nsock-pcap: can't open pcap! are you root?";
		}

		fprintf(stderr, "pcap_open_live(%s, %d, %d, %d) FAILED. Reported error: %s.  Will wait %d seconds then retry.\n",
			pcap_device, snaplen, promisc, to_ms, err0r, 4*failed);	
		sleep(4* failed);
	}while(1);

	e = nsock_pcap_set_filter(mp->pt, pcap_device, bpf);
	if(e) return e;
	
	
	#ifdef WIN32
	/* We want any responses back ASAP */
	pcap_setmintocopy(mp->pt, 1);
	#endif
	
	mp->l3_offset = nsock_pcap_get_l3_offset(mp->pt, &datalink);
	mp->snaplen = snaplen;
	mp->datalink = datalink;
	mp->pcap_device = strdup(pcap_device);
	#ifdef PCAP_CAN_DO_SELECT
	mp->pcap_desc = pcap_get_selectable_fd(mp->pt);
	#else
	mp->pcap_desc = -1;
	#endif
	mp->readsd_count = 0;

	/* Without setting this ioctl, some systems (BSDs, though it depends on
	   the release) will buffer packets in non-blocking mode and only
	   return them in a bunch when the buffer is full. Setting the ioctl
	   makes each one be delivered immediately. This is how Linux works by
	   default. See the comments surrounding the ssetting of BIOCIMMEDIATE
	   in libpcap/pcap-bpf.c. */
#ifdef BIOCIMMEDIATE
	if (mp->pcap_desc != -1) {
		int immediate = 1;
		if (ioctl(mp->pcap_desc, BIOCIMMEDIATE, &immediate) < 0)
			fatal("Cannot set BIOCIMMEDIATE on pcap descriptor");
	}
#endif

	/* Set device non-blocking */
	if(pcap_setnonblock(mp->pt, 1, err0r) < 0){
		/* I can't do select() on pcap! blockig + no_select is fatal */
		if(mp->pcap_desc < 0){
			Snprintf(errorbuf, sizeof(errorbuf),"nsock-pcap: Failed to set pcap descriptor on device %s to nonblocking state: %s", pcap_device, err0r);
			return errorbuf;
		}
			
		/* When we use bsd hack we also need to set non-blocking */
		#ifdef PCAP_BSD_SELECT_HACK
		Snprintf(errorbuf, sizeof(errorbuf),"nsock-pcap: Failed to set pcap descriptor on device %s to nonblocking state: %s", pcap_device, err0r);
		return errorbuf;
		#endif
		
		/* in other case, we can accept blocking pcap */
		fprintf(stderr, "Failed to set pcap descriptor on device %s to nonblocking state: %s", pcap_device, err0r);
	}
  	
	if (ms->tracelevel > 0)
  		nsock_trace(ms, "PCAP created successfully on device '%s' (pcap_desc=%i bsd_hack=%i to_valid=%i l3_offset=%i) (IOD #%li)", 
  		pcap_device,
  		mp->pcap_desc,
  		#if PCAP_BSD_SELECT_HACK
  		1,
  		#else
  		0,
  		#endif
  		#if PCAP_RECV_TIMEVAL_VALID
  		1,
  		#else
  		0,
  		#endif 
  		mp->l3_offset,
  		nsi->id);
  	return NULL;
}

char *nsock_pcap_set_filter(pcap_t *pt, const char *device, const char *bpf)
{
	struct bpf_program fcode;
  	static char errorbuf[128]; 
  
	// log_write(LOG_STDOUT, "Packet capture filter (device %s): %s\n", device, buf);
  
  	if (pcap_compile(pt, &fcode, (char*)bpf, 1, 0) < 0){
		Snprintf(errorbuf, sizeof(errorbuf), "Error compiling our pcap filter: %s\n", pcap_geterr(pt));
		return errorbuf;
  	}
		
	if (pcap_setfilter(pt, &fcode) < 0 ){
		Snprintf(errorbuf, sizeof(errorbuf),"Failed to set the pcap filter: %s\n", pcap_geterr(pt));
		return errorbuf;
	}
		
  	pcap_freecode(&fcode);
  	return NULL;
}

static int nsock_pcap_get_l3_offset(pcap_t *pt, int *dl){
	int datalink;
	unsigned int offset = 0;
	
	/* New packet capture device, need to recompute offset */
	if ( (datalink = pcap_datalink(pt)) < 0)
		fatal("Cannot obtain datalink information: %s", pcap_geterr(pt));

	/* NOTE: IF A NEW OFFSET EVER EXCEEDS THE CURRENT MAX (24), ADJUST
	MAX_LINK_HEADERSZ in tcpip.h */
	switch(datalink) {
	case DLT_EN10MB: offset = 14; break;
	case DLT_IEEE802: offset = 22; break;
	#ifdef __amigaos__
	case DLT_MIAMI: offset = 16; break;
	#endif
	#ifdef DLT_LOOP
	case DLT_LOOP:
	#endif
	case DLT_NULL: offset = 4; break;

	case DLT_SLIP:
	#ifdef DLT_SLIP_BSDOS
	case DLT_SLIP_BSDOS:
	#endif
	#if (FREEBSD || OPENBSD || NETBSD || BSDI || MACOSX)
	offset = 16;break;
	#else
	offset = 24;break; /* Anyone use this??? */
	#endif
	
	case DLT_PPP: 
	#ifdef DLT_PPP_BSDOS
	case DLT_PPP_BSDOS:
	#endif
	#ifdef DLT_PPP_SERIAL
	case DLT_PPP_SERIAL:
	#endif
	#ifdef DLT_PPP_ETHER
	case DLT_PPP_ETHER:
	#endif
	#if (FREEBSD || OPENBSD || NETBSD || BSDI || MACOSX)
	offset = 4;break;
	#else
	  #ifdef SOLARIS
	    offset = 8;break;
	  #else
	    offset = 24;break; /* Anyone use this? */
	  #endif /* ifdef solaris */
	#endif /* if freebsd || openbsd || netbsd || bsdi */
	#ifdef DLT_RAW
	case DLT_RAW: offset = 0; break;
	#endif /* DLT_RAW */
	case DLT_FDDI: offset = 21; break;
	#ifdef DLT_ENC
	case DLT_ENC: offset = 12; break;
	#endif /* DLT_ENC */
	#ifdef DLT_LINUX_SLL
	case DLT_LINUX_SLL: offset = 16; break;
	#endif
	#ifdef DLT_IPNET
	case DLT_IPNET: offset = 24; break;
	#endif /* DLT_IPNET */

	default: /* Sorry, link type is unknown. */
		fatal("Unknown datalink type %d.\n", datalink);
	}
	if(dl)
	  *dl = datalink;
	return(offset);
}


/*
 * Requests exacly one packet to be captured.
 * */
nsock_event_id nsock_pcap_read_packet(nsock_pool nsp, nsock_iod nsiod, 
			 nsock_ev_handler handler, int timeout_msecs,
			 void *userdata)
{
	msiod *nsi = (msiod *) nsiod;
	mspool *ms = (mspool *) nsp;
	msevent *nse;

	nse = msevent_new(ms, NSE_TYPE_PCAP_READ, nsi, timeout_msecs, handler, userdata);
	assert(nse);

	if (ms->tracelevel > 0) {
		nsock_trace(ms, "Pcap read request from IOD #%li  EID %li",  
		  nsi->id, nse->id);
	}
  
	nsp_add_event(ms, nse);
  
	return nse->id;
}

/*
 * Remember that pcap descriptor is in nonblocking state. */
int do_actual_pcap_read(msevent *nse)
{
	msiod *iod = nse->iod;
	mspcap *mp = (mspcap *) iod->pcap;
	
	nsock_pcap npp;
	nsock_pcap *n;
	struct pcap_pkthdr *pkt_header;
	const unsigned char *pkt_data = NULL;
	int rc;
	memset(&npp, 0, sizeof(nsock_pcap));
	
	if (nse->iod->nsp->tracelevel > 2)
  		nsock_trace(nse->iod->nsp, "PCAP do_actual_pcap_read TEST (IOD #%li) (EID #%li)",
  		nse->iod->id, nse->id);

	assert( FILESPACE_LENGTH(&(nse->iobuf)) == 0 );
	
	rc = pcap_next_ex(mp->pt, &pkt_header, &pkt_data);
	switch(rc){
	case 1: /* read good packet  */
		#ifdef PCAP_RECV_TIMEVAL_VALID
		npp.ts     = pkt_header->ts;
		#else
		/* on these platforms time received from pcap is invalid. It's better to set current time */
		memcpy(&npp.ts, nsock_gettimeofday(), sizeof(struct timeval));
		#endif
		npp.len    = pkt_header->len;
		npp.caplen = pkt_header->caplen;
		npp.packet = pkt_data;
		fscat(&(nse->iobuf), (char*)&npp, sizeof(npp));
		fscat(&(nse->iobuf), (char*)pkt_data, npp.caplen);
		n = (nsock_pcap *) FILESPACE_STR(&(nse->iobuf));
		n->packet = (unsigned char*)FILESPACE_STR(&(nse->iobuf))+sizeof(npp);
		if (nse->iod->nsp->tracelevel > 2)
	  		nsock_trace(nse->iod->nsp, "PCAP do_actual_pcap_read READ (IOD #%li) (EID #%li) size=%i",
	  		nse->iod->id, nse->id, pkt_header->caplen);
		return(1);
	case 0: /* timeout */
		return(0);
	case -1: /* error */
		fatal("pcap_next_ex() fatal error while reading from pcap: %s\n", pcap_geterr(mp->pt));
		break;
	case -2: /* no more packets in savefile (if reading from one) */
	default:
		assert(0);
	}
  	return 0;
}

void nse_readpcap(nsock_event nsee,
	const unsigned char **l2_data, size_t *l2_len,
	const unsigned char **l3_data, size_t *l3_len,
	size_t *packet_len, struct timeval *ts) 
{
	msevent *nse = (msevent *)nsee;
	msiod  *iod = nse->iod;
	mspcap *mp = (mspcap *) iod->pcap;

	size_t l2l;
	size_t l3l;
	
	nsock_pcap *n = (nsock_pcap *) FILESPACE_STR(&(nse->iobuf));
	if(FILESPACE_LENGTH(&(nse->iobuf)) < sizeof(nsock_pcap)){
		if(l2_data) *l2_data = NULL;
		if(l2_len ) *l2_len  = 0;
		if(l3_data) *l3_data = NULL;
		if(l3_len ) *l3_len  = 0;
		if(packet_len) *packet_len = 0;
		return;
	}
	
	l2l = MIN(mp->l3_offset, n->caplen);
	l3l = MAX(0, n->caplen-mp->l3_offset);
	
	if(l2_data) *l2_data = n->packet;
	if(l2_len ) *l2_len  = l2l;
	if(l3_data) *l3_data = l3l>0? n->packet+l2l : NULL;
	if(l3_len ) *l3_len  = l3l;
	if(packet_len) *packet_len = n->len;
	if(ts) *ts = n->ts;
	return;
}

int nsi_pcap_linktype(nsock_iod nsiod){
	msiod *nsi = (msiod *) nsiod;
	mspcap *mp = (mspcap *) nsi->pcap;
	assert(mp);
	return(mp->datalink);
}

int nsi_is_pcap(nsock_iod nsiod){
	msiod *nsi = (msiod *) nsiod;
	mspcap *mp = (mspcap *) nsi->pcap;
	return(mp!=NULL);
}


#endif // HAVE_PCAP
	
